﻿using Reader.Libs.PRC.Metadata;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Reader.Libs.PRC
{
    public class MobiDoc
    {
        private MobiMetadata metaData;
        private StringBuilder bookContent;

        public void loadContent(string filePath)
        {
            try
            {
                System.IO.FileStream fs = new System.IO.FileStream(filePath, System.IO.FileMode.Open, System.IO.FileAccess.Read);
                metaData = new Reader.Libs.PRC.Metadata.MobiMetadata();
                metaData.readMetaData(fs);

                bookContent = new StringBuilder();
                if (metaData != null)
                {
                    bool multibyte = false;
                    int trailersCount = 0;
                    if (metaData.MobiHeader.HeaderLength >= 228)
                    {
                        uint flags = metaData.MobiHeader.ExtDataFlag;
                        multibyte = ((flags & 1) != 0);
                        while (flags > 1)
                        {
                            if (0 != (flags & 2))
                                trailersCount++;
                            flags = flags >> 1;
                        }
                    }

                    int recordNum = metaData.PalmDocHeader.RecordCount;
                    for (int i = 1; i <= recordNum; i++)
                    {
                        PDBHead.RecordInfo curRC = metaData.PDBHeader.recordInfoList[i];
                        PDBHead.RecordInfo nxtRC = metaData.PDBHeader.recordInfoList[i + 1];
                        uint raw_len = nxtRC.RecordDataOffset - curRC.RecordDataOffset;
                        byte[] raw_data = new byte[raw_len];
                        fs.Read(raw_data, 0, raw_data.Length);
                        if (raw_data == null)
                            return;
                        uint real_len = GetRealRecordSize(raw_data, raw_len, trailersCount, multibyte);
                        if ((int)real_len == 0)
                            return;
                        loadRecord(raw_data, real_len);
                    }
                }
                fs.Close();
            }
            catch (System.Exception)
            {
                throw;
            }
        }

        private void loadRecord(byte[] raw_data, uint real_len)
        {
            try
            {
                if (bookContent == null)
                    bookContent = new StringBuilder();
                Encoding en = Encoding.Default;
                if (metaData.MobiHeader.TextEncoding == 65001)
                    en = Encoding.UTF8;
                if (metaData.PalmDocHeader.Compression == 1)
                {
                    bookContent.Append(en.GetString(raw_data));
                }
                else if (metaData.PalmDocHeader.Compression == 2)
                {
                    byte[] unCompress_Data = Paldoc_Uncompress(raw_data, real_len);
                    bookContent.Append(en.GetString(unCompress_Data));
                }
                else
                    throw new Exception("Unknow compress type");
            }
            catch (Exception) { throw; }
        }

        private uint GetRealRecordSize(byte[] raw_data, uint rec_len, int trailersCount, bool multibyte)
        {
            try
            {
                for (int i = 0; i < trailersCount; i++)
                {
                    if (rec_len < 4)
                        return 0;
                    uint n = 0;
                    for (int j = 0; j < 4; j++)
                    {
                        byte v = raw_data[rec_len - 4 + j];
                        if (0 != (v & 0x80))
                            n = 0;
                        n = (n << 7) | (byte)(v & 0x7f);
                    }
                    if (n > rec_len)
                        return 0;
                    rec_len -= (uint)n;
                }

                if (multibyte)
                {
                    if (0 == rec_len)
                        return 0;
                    byte m = (byte)((raw_data[rec_len - 1] & 3) + 1);
                    if (m > rec_len)
                        return 0;
                    rec_len -= m;
                }

                return rec_len;
            }
            catch (Exception)
            {    
                return 0;
            }
            
        }

        public byte[] Paldoc_Uncompress(byte[] src, uint real_len)
        {
            try
            {
                int i = 0, o = 0;
                string raw = Encoding.Default.GetString(src);
                byte[] output = new byte[real_len];
                while (i < real_len)
                {
                    Byte c = src[i++];
                    char cha = (char)c;
                    if ((c >= 1) && (c <= 8))
                    {
                        if (o + c > output.Length - 1)
                            Array.Resize(ref output, o + c);
                        while (c > 0)
                        {
                            if (i < real_len)
                            {
                                output[o++] = src[i++];
                                c--;
                            }
                            else break;
                        }
                    }
                    else if (c < 128)
                    {
                        if (o + 1 > output.Length - 1)
                            Array.Resize(ref output, o + 1);
                        output[o++] = c;
                    }

                    else if (c >= 192)
                    {
                        if (o + 2 > output.Length - 1)
                            Array.Resize(ref output, o + 2);
                        output[o++] = (byte)(' ');
                        output[o++] = (byte)(c ^ 0x80);
                    }
                    else if ((c >= 128) && (c < 192))
                    {
                        if (i + 1 < real_len)
                        {
                            UInt16 c2 = (UInt16)((c << 8) | src[i]);
                            UInt16 di = (UInt16)((c2 >> 3) & 0x07ff);

                            for (byte n = (byte)((c2 & 7) + 3); n > 0; n--)
                            {
                                if (di <= o && di > 0)
                                {
                                    if (o + n > output.Length - 1)
                                        Array.Resize(ref output, o + 1);
                                    output[o++] = output[o - 1 - di];
                                }
                            }
                            i++;
                        }
                    }
                }
                return output;
            }
            catch (Exception)
            {
                throw new Exception("Cannot Decompress Record");
            }
            
        }

        public void writeContent(string filePath)
        {
            try
            {
                using (FileStream fs = new System.IO.FileStream(filePath, System.IO.FileMode.Create, System.IO.FileAccess.Write))
                {
                    metaData = new Reader.Libs.PRC.Metadata.MobiMetadata();
                    metaData.writeMetaData(fs, bookContent.ToString());

                    byte[] ct = System.Text.Encoding.UTF8.GetBytes(BookContent);
                    fs.Write(ct, 0, ct.Length);

                    byte[] tmp = new byte[4096 * 4];
                    fs.Write(tmp, 0, tmp.Length);
                    fs.Close();
                }
            }
            catch (System.Exception)
            {
                throw;
            }
        }

        public string BookContent
        {
            get { return bookContent.ToString(); }
            set 
            {
                if (this.bookContent == null)
                    this.bookContent = new StringBuilder();
                this.bookContent.Append(value); 
            }
        }
    }
}
